<html>
<head>
  <title>一个使用 asyncio 协程的网络爬虫</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="Evernote Windows/307027 (zh-CN, DDL); Windows/6.1.0 (Win32);"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="625"/>
<h1>一个使用 asyncio 协程的网络爬虫</h1>

<div>
<span><div><div><div><span style="color: rgb(28, 51, 135); font-weight: bold;"> </span><span style="font-size: 18pt; color: rgb(28, 51, 135); font-weight: bold;">原型式的异步应用</span></div><div>         这一章我们将实现一个简单的网络爬虫。这个爬虫只是一个原型式的异步应用，因为它等待许多响应而只做少量的计算。</div><div>        一次爬的网页越多，它就能越快的完成任务。如果它为每个动态的请求启动一个线程的话，随着并发请求数量的增加，它会在耗尽套接字之前，耗尽内存或者线程相关的资源。使用异步 I/O 可以避免这个的问题。</div><div><br/></div><div><span style="font-size: 16pt; color: rgb(173, 0, 0);">三个阶段展示这个例子：</span></div><div><ul><li>首先，我们会实现一个事件循环并用这个事件循环和回调来勾画出一只网络爬虫。</li><li>然后，由于 Python 的协程不仅有效而且可扩展，我们将用 Python 的生成器函数实现一个简单的协程。</li><li>最后一个阶段，我们将使用 Python 标准库“asyncio”中功能完整的协程， 并通过异步队列完成这个网络爬虫。</li></ul><div><br/></div></div><div><br/></div><h3><span style="font-size: 18pt; color: rgb(28, 51, 135);">任务</span></h3><div>网络爬虫寻找并下载一个网站上的所有网页，也许还会把它们存档，为它们建立索引。</div><div>从根 URL 开始，它获取每个网页，解析出没有遇到过的链接加到队列中。</div><div>当网页没有未见到过的链接并且队列为空时，它便停止运行。</div><div><br/></div><div>可以通过同时下载大量的网页来加快这一过程。</div><div>当爬虫发现新的链接，它使用一个新的套接字并行的处理这个新链接，解析响应，添加新链接到队列。</div><div>并发很大时，可能会导致性能下降，所以我们会限制并发的数量，在队列保留那些未处理的链接，直到一些正在执行的任务完成。</div><div><br/></div><div><br/></div><h3><span style="font-size: 16pt; color: rgb(28, 51, 135);">传统方式</span><span style="font-size: 16pt; color: rgb(28, 51, 135); font-weight: bold;">爬虫并发</span></h3><div>        传统的做法是创建一个线程池，每个线程使用一个套接字在一段时间内负责一个网页的下载。比如，下载 <a href="http://xkcd.com/">xkcd.com</a> 网站的一个网页：</div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">def fetch(url) :</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    sock = socket.socket()</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    sock.connect(('xkcd.com', 80))</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    sock.send(request.encode('ascii'))</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    response = b' '</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    chunk = sock.recv(4096)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    while chunk:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">        response += chunk</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">        chunk = sock.recv(4096)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    # Page is now downloaded.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    links = parse_links(response)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    q.add(links)</span></div></div><div>        套接字操作默认是阻塞的：当一个线程调用一个类似 connect 和 recv 方法时，它会阻塞，直到操作完成。（即使是 send 也能被阻塞，比如接收端在接受外发消息时缓慢而系统的外发数据缓存已经满了的情况下）因此，为了同一时间内下载多个网页，我们需要很多线程。一个复杂的应用会通过线程池保持空闲的线程来分摊创建线程的开销。同样的做法也适用于套接字，使用连接池。</div><div><br/></div><div><br/></div><div><br/></div><div><font style="font-size: 18pt;"><span style="font-size: 18pt; color: rgb(28, 51, 135); font-weight: bold; line-height: 1.45;">异步</span></font></div><div><font style="font-size: 16pt;"><span style="font-size: 16pt; color: rgb(173, 0, 0); font-weight: bold;">异步 I/O 框架在一个线程中完成并发操作。</span></font></div><div>异步框架使用<span style="font-style: italic;">非阻塞</span>套接字。异步爬虫中，我们在发起到服务器的连接前把套接字设为非阻塞：</div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">sock = socket.socket()</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">sock.setblocking(False)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">try:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    sock.connect(('xkcd.com', 80))</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">except BlockingIOError:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12pt; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    pass</span></div></div><div><br/></div><div>        对一个非阻塞套接字调用 connect 方法会立即抛出异常，即使它可以正常工作。</div><div>现在我们的爬虫需要一种知道连接何时建立的方法，这样它才能发送 HTTP 请求。我们可以简单地使用循环来重试：</div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">request = 'GET {] HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">encoded = request.encode('ascii')</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">while True:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    try:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">        sock.send(encoded)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">        break # Done</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">    except OSError as e:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">        pass</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px;">print('sent')</span></div></div><div>这种方法不仅消耗 CPU，也不能有效的等待<span style="font-style: italic;">多个</span>套接字。</div><div><br/></div><div>解决方法是 select，这是一个 C 函数，它在一个或一组非阻塞套接字上等待事件发生。</div><div>现在，互联网应用大量连接的需求，导致 select 被 poll 所代替，在 BSD 上的实现是 kqueue ，在 Linux 上是 epoll。</div><div><br/></div><div><font style="font-size: 14pt;"><span style="font-size: 14pt; color: rgb(173, 0, 0); font-weight: bold;">Python 3.4 的 DefaultSelector 会使用你系统上最好的 select 类函数。要注册一个网络 I/O 事件的提醒，我们会创建一个非阻塞套接字，并使用默认 selector 注册它。</span></font></div><div><br/></div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">from selectors import DefaultSelector, EVENT_WRITE</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">selector = DefaultSelector()</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">sock = socket.socket()</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">sock.setblocking(False)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">try:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    sock.connect('xkcd.com', 80))</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">except BlockingIOError:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    pass</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">def connected():</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    selector.unregister(sock.fileno())</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    print('connected!')</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">selector.register(sock.fileno(), EVENT_WRITE,connected)</span></div></div><div><br/></div><div>我们不理会这个伪造的错误，调用 selector.register，传递套接字文件描述符和一个表示我们想要监听什么事件的常量表达式。为了当连接建立时收到提醒，我们使用 EVENT_WRITE ：它表示什么时候这个套接字可写。我们还传递了一个 Python 函数 connected，当对应事件发生时被调用。这样的函数被称为<span style="font-style: italic;">回调</span>。</div><div><br/></div><div><font style="font-size: 12pt;"><span style="font-size: 12pt; color: rgb(173, 0, 0); font-weight: bold;">connected 回调函数</span></font></div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">def loop():</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    while True:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        events = selector.select()</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        for event_key, event_mask in events:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">            callback = event_key.data</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">            callback()</span></div></div><div><br/></div><div><br/></div><div><br/></div><div><font style="font-size: 12pt;"><span style="font-size: 12pt; color: rgb(50, 135, 18); font-weight: bold;">到目前为止我们展现了什么？我们展示了如何开始一个 I/O 操作和当操作准备好时调用回调函数。异步</span><span style="font-size: 12pt; color: rgb(50, 135, 18); font-style: italic; font-weight: bold;">框架</span><span style="font-size: 12pt; color: rgb(50, 135, 18); font-weight: bold;">，它在单线程中执行并发操作，其建立在两个功能之上，非阻塞套接字和事件循环。</span></font></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><h3><span style="font-size: 18pt; color: rgb(28, 51, 135);">回调</span></h3><div><ul><li><span style="line-height: 1.45;">首先，我们有一个尚未获取的 URL 集合，和一个已经解析过的 URL 集合。</span></li></ul><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">urls_todo = set(['/'])</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">seen_urls = set(['/'])</span></div></div></div><div>       <span style="font-size: 12pt; color: rgb(28, 51, 135);">seen_urls</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">集合包括</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">urls_todo</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">和已经完成的 URL。用根 URL</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">/</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">初始化它们。</span></div><div><span style="font-size: 12pt; color: rgb(28, 51, 135);">获取一个网页需要一系列的回调。在套接字连接建立时会触发</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">connected</span> <span style="font-size: 12pt; color: rgb(28, 51, 135);">回调，它向服务器发送一个 GET 请求。但是它要等待响应，所以我们需要注册另一个回调函数；当该回调被调用，它仍然不能读取到完整的请求时，就会再一次注册回调，如此反复。</span><span style="font-size: 12pt; color: rgb(28, 51, 135);"> </span></div><div><br/></div><div><ul><li>让我们把这些回调放在一个 Fetcher 对象中，它需要一个 URL，一个套接字，还需要一个地方保存返回的字节：</li></ul><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">class Fetcher:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    def __init__(self, url):</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        self.response = b'' # Empty array of bytes.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        self.url = url</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        self.sock = None</span></div></div></div><div><br/></div><div>入口点在 Fetcher.fetch :</div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;"># Method on Fetcher class.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">def fetch(self):</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    self.sock = socket.socket()</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    self.sock.setblocking(False)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    try:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        self.sock.connect(('xkcd.com', 80))</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    except BlockingIOError:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        pass</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    </span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    # Register next callback.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    selector.register(self.sock.fileno(),</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">                      EVENT_WRITE,</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">                      self.connected)</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">def connected（self, key, mask):</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    print('connected!')</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    selector.unregister(key,fd)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(self.url)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    self.sock.send(request.encode('ascii'))</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    # Register the next callback.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    selector.register(key.fd,</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">                      EVENT_READ,</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">                      self.read_response)</span></div></div><div><br/></div><div><span style="color: rgb(50, 135, 18); font-weight: bold;">connected这个方法发送一个 GET 请求。一个真正的应用会检查 send 的返回值，以防所有的信息没能一次发送出去。但是我们的请求很小，应用也不复杂。它只是简单的调用 send，然后等待响应。当然，它必须注册另一个回调并把控制权交给事件循环。接下来也是最后一个回调函数 read_response，它处理服务器的响应：</span></div><div><br/></div><div style="box-sizing: border-box; padding: 8px; font-size: 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902);"><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;"># Method on Fetcher class.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">def read_response(self, key, mask):</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    global stopped</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    </span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    chunk = self.sock.recv(4096)    # 4k chunk size.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    if chunk:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        self.response += chunk</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">    else:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        selector.unregister(key.fd) # Done reading.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        links = self.parse_links()</span></div><div><br/></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        # Python set-logic:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        for link in links.difference(seen_urls):</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">            urls_todo.add(link)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">            Fetcher(link).fetch() # &lt;- New Fetcher.</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        </span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        seen_urls.update(links)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        urls_todo.remove(self.url)</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">        if not urls_todo:</span></div><div><span style="background-color: rgb(251, 250, 248); font-size: 12px; color: rgb(51, 51, 51); font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;">            stopped = Truez</span></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div></div><div><br/></div><div>这个回调在每次 selector 发现套接字<span style="font-style: italic;">可读</span>时被调用，可读有两种情况：套接字接受到数据或它被关闭。</div><div>这个回调函数从套接字读取 4K 数据。如果不到 4k，那么有多少读多少。如果比 4K 多，chunk 中只包 4K 数据并且这个套接字保持可读，这样在事件循环的下一个周期，会再次回到这个回调函数。当响应完成时，服务器关闭这个套接字，chunk 为空。</div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><font style="font-size: 18pt;"><span style="color: rgb(28, 51, 135); font-size: 18pt; font-weight: bold;">协程</span></font></div><div>我们可以写出这样的异步代码，它既有回调方式的高效，也有多线程代码的简洁。这个结合是同过一种称为协程（coroutine）的模式来实现的。</div></div><div><br/></div><h3><span style="color: rgb(28, 51, 135); font-size: 18pt;">生成器如何工作</span></h3><div>在你理解生成器之前，你需要知道普通的 Python 函数是怎么工作的。正常情况下，当一个函数调用一个子过程，这个被调用函数获得控制权，直到它返回或者有异常发生，才把控制权交给调用者：</div><div><br/></div><div><br/></div><div><span style="font-size: 18pt; color: rgb(28, 51, 135); font-weight: bold;">使用协程</span></div><div><font color="#328712" style="font-size: 12pt;"><b><br/></b></font></div><div><font color="#328712" style="font-size: 12pt;"><b><span>    <span>    </span></span>我们的爬虫从获取第一个网页开始，解析出链接并把它们加到队列中。此后它开始傲游整个网站，并发地获取网页。但是由于客户端和服务端的负载限制，我们希望有一个最大数目的运行的 worker，不能再多。任何时候一个 worker 完成一个网页的获取，它应该立即从队列中取出下一个链接。我们会遇到没有那么多事干的时候，所以一些 worker 必须能够暂停。一旦又有 worker 获取一个有很多链接的网页，队列会突增，暂停的 worker 立马被唤醒干活。最后，当任务完成后我们的程序必须马上退出。</b></font></div><div><br/></div><div><span>    <span>    </span></span>假如你的 worker 是线程，怎样去描述你的爬虫算法？我们可以使用 Python 标准库中的<a href="https://docs.python.org/3/library/queue.html">同步队列</a>。每次有新的一项加入，队列增加它的 “tasks” 计数器。线程 worker 完成一个任务后调用 task_done。主线程阻塞在 Queue.join，直到“tasks”计数器与 task_done 调用次数相匹配，然后退出。</div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div><div><br/></div></div><div><br/></div></span>
</div></body></html> 